
/*
 * Copyright (c) 2010 Sergey N Frolov
 * 
 * See LICENSE file
 */

#include "ngx_javascript.h"

/*
 * Read GET, POST or COOKIE request, depending on arg. Register variables
 */
JSBool ngx_javascript_read_request(JSContext *jcx, JSObject *obj, int arg) {
	ngx_javascript_client_t *client = JS_GetContextPrivate(jcx);
	char *res = NULL, *var, *val, *separator = NULL;
	char *strtok_buf = NULL;
	int free_buffer = 0;
	if (arg == NGX_JAVASCRIPT_PARSE_GET) {
		if (client->query_string == NULL || client->query_string[0] == '\0') return JS_TRUE;
		res = JS_strdup(jcx, client->query_string);
		if (res == NULL) {
			return JS_FALSE;
		} else {
			free_buffer = 1;
		}
	}
	if (arg == NGX_JAVASCRIPT_PARSE_COOKIE) {
		if (client->cookie == NULL || client->cookie[0] == '\0') return JS_TRUE;
		res = JS_strdup(jcx, client->cookie);
		if (res == NULL) {
			return JS_FALSE;
		} else {
			free_buffer = 1;
		}
	}
	if (arg == NGX_JAVASCRIPT_PARSE_POST) {
		if (client->body_type == NGX_JAVASCRIPT_PARSE_FORM) {
			if (client->body != NULL && client->content_length > 0) {
				return ngx_javascript_read_form(jcx, obj, client);
			}
		} else if (client->body_type == NGX_JAVASCRIPT_PARSE_DATA) {
			if (client->body != NULL && client->content_length > 0) {
				return ngx_javascript_read_data(jcx, obj, client);
			} else if (client->body_fd >= 0) {
				return ngx_javascript_read_data_file(jcx, obj, client);
			}
		}
		return JS_TRUE;
	}
	switch (arg) {
		case NGX_JAVASCRIPT_PARSE_GET:
		case NGX_JAVASCRIPT_PARSE_STRING:
			separator = "&;";
			break;
		case NGX_JAVASCRIPT_PARSE_COOKIE:
			separator = ";\0";
			break;
	}
	var = ngx_javascript_string_token(res, separator, &strtok_buf);
	while (var) {
		val = strchr(var, '=');
		if (arg == NGX_JAVASCRIPT_PARSE_COOKIE) {
			while (*var == ' ' || *var == '\t' || *var == '\r' || *var == '\n') {
				var ++;
			}
			if (var == val || *var == '\0') {
				goto next_cookie;
			}
		}
		if (val) {
			char *dst;
			size_t val_len;
			size_t dst_len;
			*val ++ = '\0';
			val_len = strlen(val);
			dst = JS_malloc(jcx, val_len + 1);
			dst_len = (size_t) dst;
			ngx_unescape_uri(&dst, &val, val_len, NGX_UNESCAPE_URI);
			dst_len = (size_t) dst - dst_len;
			*dst = '\0';
			dst -= dst_len;
			val -= val_len;
			ngx_javascript_register_variable(jcx, obj, var, strlen(var), dst, dst_len);
			JS_free(jcx, dst);
		}
		next_cookie:
		var = ngx_javascript_string_token(NULL, separator, &strtok_buf);
	}
	if (free_buffer) {
		JS_free(jcx, res);
	}
	return JS_TRUE;
}

/*
 * Register javascript variable. Check if variable name contains '\0', '[' or ']', if it has non-zero length and is a correct reference to a property
 */
JSBool ngx_javascript_register_variable(JSContext *jcx, JSObject *obj, char *var, size_t var_len, char *val, size_t val_len) {
	char *dst;
	size_t dst_len;
	size_t i;
	char c;
	int ok;
	jsval result;
	dst = JS_malloc(jcx, var_len + 1);
	memset(dst, 0, var_len + 1);
	dst_len = (size_t) dst;
	ngx_unescape_uri(&dst, &var, var_len, NGX_UNESCAPE_URI);
	dst_len = (size_t) dst - dst_len;
	*dst = '\0';
	dst -= dst_len;
	var -= var_len;
	ok = 1;
	if (!dst_len) {
		ok = 0;
	} else if (dst[0] == '[' || dst[dst_len - 1] == '[') {
		ok = 0;
	} else {
		int open_first = 0;
		int open_count = 0;
		int close_count = 0;
		for (i = 0; i < dst_len; i ++) {
			c = dst[i];
			if (c == '\0') {
				ok = 0;
				break;
			} else if (c == '[') {
				open_count ++;
				if (!open_first) {
					if (close_count) {
						ok = 0;
						break;
					}
					open_first = i;
				}
			} else if (c == ']') {
				close_count ++;
				if (i != dst_len - 1 && dst[i + 1] != '[') {
					ok = 0;
					break;
				}
			}
		}
		if (ok) {
			if (open_count == close_count) {
				if (open_first) {
					ngx_javascript_register_variable_r(jcx, obj, dst, dst_len, val, val_len);
					ok = 0;
				}
			} else {
				ok = 0;
			}
		}
	}
	if (ok) {
		result = STRING_TO_JSVAL(JS_NewStringCopyN(jcx, (char *) val, val_len));
		JS_DefineProperty(jcx, obj, (char *) dst, result, NULL, NULL, JSPROP_ENUMERATE);
	}
	JS_free(jcx, dst);
	return JS_TRUE;
}

/*
 * Register javascript variable in array. That is, variable name contains '[' and ']' and is a correct reference to array element
 */
JSBool ngx_javascript_register_variable_r(JSContext *jcx, JSObject *obj, char *var, size_t var_len, char *val, size_t val_len) {
	size_t i;
	char *p = var;
	JSObject *o, *old;
	jsval result;
	o = old = NULL;
	for (i = 0; i < var_len; i ++) {
		if (var[i] == '[') {
			var[i] = '\0';
			if (JS_GetProperty(jcx, obj, (char *) p, &result) == JS_TRUE && (JSVAL_IS_OBJECT(result))) {
				o = old = JSVAL_TO_OBJECT(result);
			} else {
				o = old = JS_NewArrayObject(jcx, 0, NULL);
				result = OBJECT_TO_JSVAL(o);
				JS_DefineProperty(jcx, obj, (char *) p, result, NULL, NULL, JSPROP_ENUMERATE);
			}
			break;
		}
	}
	if (!o) return JS_TRUE;
	p = var + i + 1;
	for (i = i + 1; i < var_len; i ++) {
		if (var[i] == ']') {
			var[i] = '\0';
			if (p[0] == '\0') {
				jsuint l;
				if (JS_HasArrayLength(jcx, old, &l) == JS_FALSE) {
					l = 0;
				}
				JS_SetArrayLength(jcx, old, l + 1);
				if (i == var_len - 1) {
					result = STRING_TO_JSVAL(JS_NewStringCopyN(jcx, (char *) val, val_len));
					JS_DefineElement(jcx, old, l, result, NULL, NULL, JSPROP_ENUMERATE);
					return JS_TRUE;
				} else {
					o = JS_NewArrayObject(jcx, 0, NULL);
					result = OBJECT_TO_JSVAL(o);
					JS_DefineElement(jcx, old, l, result, NULL, NULL, JSPROP_ENUMERATE);
					p = var + i + 2;
					old = o;
				}
			} else {
				if (i == var_len - 1) {
					result = STRING_TO_JSVAL(JS_NewStringCopyN(jcx, (char *) val, val_len));
					JS_DefineProperty(jcx, old, (char *) p, result, NULL, NULL, JSPROP_ENUMERATE);
					return JS_TRUE;
				} else {
					if (JS_GetProperty(jcx, old, (char *) p, &result) == JS_TRUE && (JSVAL_IS_OBJECT(result))) {
						o = JSVAL_TO_OBJECT(result);
					} else {
						o = JS_NewArrayObject(jcx, 0, NULL);
						result = OBJECT_TO_JSVAL(o);
						JS_DefineProperty(jcx, old, (char *) p, result, NULL, NULL, JSPROP_ENUMERATE);
					}
					p = var + i + 2;
					old = o;
				}
			}
		}
	}
	return JS_TRUE;
}

/*
 * Simple string tokenizer by delimiter
 */
char *ngx_javascript_string_token(char *s, char *delim, char **last) {
	char *spanp;
	int c, sc;
	char *tok;
	if (s == NULL && (s = *last) == NULL) {
		return NULL;
	}
	cont:
	c = *s ++;
	for (spanp = delim; (sc = *spanp ++) != 0; ) {
		if (c == sc) {
			goto cont;
		}
	}
	if (c == 0) {
		*last = NULL;
		return NULL;
	}
	tok = s - 1;
	for (;;) {
		c = *s ++;
		spanp = delim;
		do {
			if ((sc = *spanp ++) == c) {
				if (c == 0) {
					s = NULL;
				} else {
					char *w = s - 1;
					*w = '\0';
				}
				*last = s;
				return tok;
			}
		} while (sc != 0);
	}
}

void ngx_unescape_uri(char **dst, char **src, size_t size, unsigned int type) {
    char *d, *s, ch, c, decoded;
    enum {
        sw_usual = 0,
        sw_quoted,
        sw_quoted_second
    } state;
    d = *dst;
    s = *src;
    state = 0;
    decoded = 0;
    while (size--) {
        ch = *s++;
        switch (state) {
        case sw_usual:
            if (ch == '?' && (type & (NGX_UNESCAPE_URI|NGX_UNESCAPE_REDIRECT))) {
                *d++ = ch;
                goto done;
            }
            if (ch == '%') {
                state = sw_quoted;
                break;
            }
            *d++ = ch;
            break;
        case sw_quoted:
            if (ch >= '0' && ch <= '9') {
                decoded = (ch - '0');
                state = sw_quoted_second;
                break;
            }
            c = (ch | 0x20);
            if (c >= 'a' && c <= 'f') {
                decoded = (c - 'a' + 10);
                state = sw_quoted_second;
                break;
            }
            state = sw_usual;
            *d++ = ch;
            break;
        case sw_quoted_second:
            state = sw_usual;
            if (ch >= '0' && ch <= '9') {
                ch = ((decoded << 4) + ch - '0');
                if (type & NGX_UNESCAPE_REDIRECT) {
                    if (ch > '%' && ch < 0x7f) {
                        *d++ = ch;
                        break;
                    }
                    *d++ = '%'; *d++ = *(s - 2); *d++ = *(s - 1);
                    break;
                }
                *d++ = ch;
                break;
            }
            c = (ch | 0x20);
            if (c >= 'a' && c <= 'f') {
                ch = ((decoded << 4) + c - 'a' + 10);
                if (type & NGX_UNESCAPE_URI) {
                    if (ch == '?') {
                        *d++ = ch;
                        goto done;
                    }
                    *d++ = ch;
                    break;
                }
                if (type & NGX_UNESCAPE_REDIRECT) {
                    if (ch == '?') {
                        *d++ = ch;
                        goto done;
                    }
                    if (ch > '%' && ch < 0x7f) {
                        *d++ = ch;
                        break;
                    }
                    *d++ = '%'; *d++ = *(s - 2); *d++ = *(s - 1);
                    break;
                }
                *d++ = ch;
                break;
            }
            break;
        }
    }
done:
    *dst = d;
    *src = s;
}



/*
 * Handle application/x-www-form-urlencoded
 */
JSBool ngx_javascript_read_form(JSContext *jcx, JSObject *obj, ngx_javascript_client_t *client) {
	char *var, *val;
	char *s = client->body;
	char *e = client->body + client->content_length;
	char *p;
	while (s < e && (p = memchr(s, '&', (e - s)))) {
		*p = '\0';
		last_post_value:
		var = s;
		if ((val = memchr(s, '=', (p - s)))) {
			char *dst;
			size_t val_len;
			size_t dst_len;
			*val ++ = '\0';
			dst = memchr(val, 0, e - val);
			val_len = dst == NULL ? e - val : dst - val;
			dst = ngx_javascript_calloc(val_len + 1);
			dst_len = (size_t) dst;
			ngx_unescape_uri(&dst, &val, val_len, NGX_UNESCAPE_URI);
			dst_len = (size_t) dst - dst_len;
			*dst = '\0';
			dst -= dst_len;
			val -= val_len;
			ngx_javascript_register_variable(jcx, obj, var, strlen(var), dst, dst_len);
			ngx_javascript_free(dst);
		}
		s = p + 1;
	}
	if (s < e) {
		p = e;
		goto last_post_value;
	}
	return JS_TRUE;
}

/*
 * Get boundary from Content-Type header of the request
 */
JSBool ngx_javascript_get_boundary(ngx_javascript_client_t *client, char **boundary_data, size_t *boundary_length) {
	char *content_type_data = client->content_type;
	unsigned int content_type_len = client->content_type_len;
	char *boundary, *boundary_end;
	size_t boundary_len;
	boundary = ngx_strlcasestrn(content_type_data, content_type_data + content_type_len, "boundary=", sizeof("boundary=") - 2);
	if (boundary == NULL) return JS_FALSE;
	boundary += sizeof("boundary=") - 1;
	boundary_len = content_type_data + content_type_len - boundary;
	if (boundary_len <= 0) return JS_FALSE;
	if (boundary[0] == '"') {
		boundary ++;
		boundary_len --;
		boundary_end = memchr(boundary, '"', boundary_len);
		if (boundary_end == NULL) return JS_FALSE;
		boundary_len = boundary_end - boundary;
		if (boundary_len <= 0) return JS_FALSE;
	} else {
		boundary_end = memchr(boundary, ',', boundary_len);
		if (boundary_end != NULL) {
			boundary_len = boundary_end - boundary;
			if (boundary_len <= 0) return JS_FALSE;
		}
	}
	if (boundary_len >= NGX_JAVASCRIPT_BUFFER1) return JS_FALSE;
	*boundary_data = boundary;
	*boundary_length = boundary_len;
	return JS_TRUE;
}

/*
 * Read specified data size from a file
 */
size_t ngx_javascript_read_from_file(ngx_javascript_multi_part_t *m, size_t s) {
	ssize_t n;
	if (m->offset >= m->length) return 0;
	if (s == NGX_JAVASCRIPT_BUFFER && m->l > s) {
		memmove(m->b, m->b + m->l - s, s);
		m->o += m->l - s;
		m->l = s;
		n = pread(m->fd, (char *) (m->b + s), s, m->offset);
	} else {
		m->o += m->l;
		m->l = 0;
		n = pread(m->fd, (char *) (m->b), s, m->offset);
	}
	if (n <= 0) return 0;
	m->offset += n;
	m->l += n;
	return n;
}

/*
 * Offset of next line in file
 */
JSBool ngx_javascript_read_line_file(ngx_javascript_multi_part_t *m, size_t s, size_t *ro, char **rp) {
	size_t n;
	char *p;
	size_t ms = s - m->o;
	p = memchr(m->b + ms, '\n', m->l - ms);
	if (p != NULL) {
		*ro = (p - m->b) + m->o + 1;
		*rp = p;
		return JS_TRUE;
	}
	if (ms > NGX_JAVASCRIPT_BUFFER) {
		ms -= m->l - NGX_JAVASCRIPT_BUFFER;
		n = ngx_javascript_read_from_file(m, NGX_JAVASCRIPT_BUFFER);
		if (n == 0) {
			*ro = m->length;
			*rp = m->b + m->l;
			return JS_FALSE;
		}
		p = memchr(m->b + ms, '\n', m->l - ms);
		if (p != NULL) {
			*ro = p - m->b + m->o + 1;
			*rp = p;
			return JS_TRUE;
		}
	}
	while ((n = ngx_javascript_read_from_file(m, NGX_JAVASCRIPT_BUFFER2)) > 0) {
		p = memchr(m->b, '\n', m->l);
		if (p != NULL) {
			*ro = p - m->b + m->o + 1;
			*rp = p;
			return JS_TRUE;
		}
	}
	*ro = m->length;
	*rp = m->b + m->l;
	return JS_FALSE;
}

/*
 * Read next boundary from a file. Set variables in m related to headers. Set offset and pointer to data
 */
JSBool ngx_javascript_read_boundary_file(ngx_javascript_multi_part_t *m, size_t s, size_t *ro, char **rp) {
	size_t p = s; // Previous start of line (offset)
	size_t n = p; // Next start of line (offset)
	size_t l = 0; // Length of line
	m->hl = 0; // Header length is 0
	m->ho = s; // Header offset is start of search position
	char *x;
	while (p < m->length) {
		ngx_javascript_read_line_file(m, p, &n, &x);
		l = n - p;
		if (l == 0) {
			// Can't read more
			return JS_FALSE;
		} else if (l == m->rn && n > s) {
			// end-of-line
			if (n - s < NGX_JAVASCRIPT_BUFFER1 && m->hl == 0) {
				// Whole header is now in read buffer
				// No header was read before
				// Copy it to header buffer
				m->hl = n - s;
				memcpy(m->h, m->b + s - m->o, m->hl);
			}
		} else if (l < NGX_JAVASCRIPT_BUFFER1 && l > m->boundary_len + 2) {
			// Whole string is in read buffer
			// Length of string is enough for boundary
			x = m->b + p - m->o;
			if (x[0] == '-' && x[1] == '-' && strncmp(x + 2, m->boundary, m->boundary_len) == 0) {
				*ro = p + 2 + m->boundary_len;
				*rp = x + 2 + m->boundary_len;
				return JS_TRUE;
			}
		}
		p = n;
	}
	return JS_FALSE;
}

/*
 * Parse multipart/form-data from a file
 */
JSBool ngx_javascript_read_data_file(JSContext *jcx, JSObject *obj, ngx_javascript_client_t *client) {
	ngx_javascript_multi_part_t mm;
	ngx_javascript_multi_part_t *m = &mm;
	char bb[NGX_JAVASCRIPT_BUFFER2];
	char hh[NGX_JAVASCRIPT_BUFFER];
	size_t p, n;
	char *x;
	char *var, *val, *name;
	size_t var_len, val_len, name_len;
	
	memset(m, 0, sizeof(ngx_javascript_multi_part_t));
	m->fd = client->body_fd;
	m->length = client->body_pos;
	m->b = bb;
	m->h = hh;
	m->client = client;
	
	if (ngx_javascript_get_boundary(client, &m->boundary, &m->boundary_len) == JS_FALSE) return JS_FALSE;
	
	if (ngx_javascript_read_from_file(m, NGX_JAVASCRIPT_BUFFER2) <= 0) return JS_FALSE;
	
	// read first boundary
	if (ngx_javascript_read_boundary_file(m, 0, &p, &x) == JS_FALSE) {
		// first boundary not found
		return JS_FALSE;
	}
	
	// determine size of end-of-line: 1 (\n) or 2 (\r\n)
	if (x[0] == '\r') {
		m->rn ++;
		x ++;
	}
	if (x[0] == '\n') {
		m->rn ++;
		x ++;
	}
	p += m->rn;
	if (m->rn == 0) return JS_FALSE;
	
	// p points to the start of chunk - right after boundary\r\n
	while (p < m->length) {
		// read chunks separated by boundaries
		var = val = name = NULL;
		var_len = val_len = name_len = 0;
		// read next boundary
		if (ngx_javascript_read_boundary_file(m, p, &n, &x) == JS_FALSE) {
			// next boundary not found
			return JS_FALSE;
		}
		if (m->hl != 0) {
			ngx_javascript_multipart_headers(m, &var, &var_len, &name, &name_len);
			if (var != NULL) {
				if (name == NULL) {
					// normal variable
					val_len = n - m->boundary_len - 2 - m->rn - m->ho - m->hl;
					if (m->ho + m->hl >= m->o && m->ho + m->hl + val_len <= m->o + m->l) {
						val = (char *) (m->b + m->ho - m->o + m->hl);
						ngx_javascript_register_variable(jcx, obj, var, var_len, val, val_len);
					} else {
						val = ngx_javascript_calloc(val_len);
						if (val == NULL) {
							return JS_FALSE;
						}
						if (pread(m->fd, val, val_len, m->ho + m->hl) > 0) {
							ngx_javascript_register_variable(jcx, obj, var, var_len, val, val_len);
						}
						ngx_javascript_free(val);
					}
				} else {
					// file upload
					ngx_javascript_register_variable(jcx, obj, var, var_len, name, name_len);
				}
			}
		}
		p = n;
		if (m->length - p < 2) break;
		if (x[0] == '-' && x[1] == '-') break;
		p += m->rn;
	}
	return JS_TRUE;
}

/*
 * Parse headers of one multi-part data and set variable name and file name
 */
JSBool ngx_javascript_multipart_headers(ngx_javascript_multi_part_t *m, char **rvar, size_t *rvar_len, char **rname, size_t *rname_len) {
	char *var, *name;
	size_t var_len, name_len;
	char *h = (char *) (m->h); // start of headers
	char *n = (char *) (m->h + m->hl); // end of headers
	var = name = NULL; // variable name, variable value, file name
	var_len = name_len = 0; // lengths
	
	while (h < n) {
		// read header line
		char *x = ngx_javascript_read_line(h, n);
		if ((int) (x - h) <= (int) (m->rn)) {
			// too short header
			break;
		}
		// check header
		if ((size_t) (x - h) >= sizeof("Content-Disposition:") && ngx_strncasecmp(h, "Content-Disposition:", sizeof("Content-Disposition:") - 1) == 0) {
			// content-disposition header
			h += sizeof("Content-Disposition:") - 1;
			// find ; in this header
			h = memchr(h, ';', x - h);
			if (h == NULL) {
				// ; not found, ignore this header and go to next
				h = x;
				continue;
			}
			h ++;
			while (h < x) {
				if (h[0] == ' ' || h[0] == '\t' || h[0] == '\r' || h[0] == '\n') h ++;
				else break;
			}
			// h now points to the start of description after ;\s*
			while (h < x) {
				if ((size_t) (x - h) >= sizeof("name=") && ngx_strncasecmp(h, "name=", sizeof("name") - 1) == 0) {
					h += sizeof("name=") - 1;
					if (h[0] == '"') {
						h ++;
						var = h;
						h = memchr(h, '"', x - h);
						if (h == NULL) {
							var = NULL;
							break;
						} else {
							var_len = h - var;
							var[var_len] = '\0';
							h ++;
							h = memchr(h, ';', x - h);
							if (h == NULL) break;
							h ++;
						}
					} else {
						var = h;
						h = memchr(h, ';', x - h);
						if (h == NULL) {
							var_len = x - var;
							var[var_len] = '\0';
							break;
						} else {
							var_len = h - var;
							var[var_len] = '\0';
							h ++;
						}
					}
				} else if ((size_t) (x - h) >= sizeof("filename=") && ngx_strncasecmp(h, "filename=", sizeof("filename") - 1) == 0) {
					h += sizeof("filename=") - 1;
					if (h[0] == '"') {
						h ++;
						name = h;
						h = memchr(h, '"', x - h);
						if (h == NULL) {
							name = NULL;
							break;
						} else {
							name_len = h - name;
							name[name_len] = '\0';
							h ++;
							h = memchr(h, ';', x - h);
							if (h == NULL) break;
							h ++;
						}
					} else {
						name = h;
						h = memchr(h, ';', x - h);
						if (h == NULL) {
							name_len = x - name;
							name[name_len] = '\0';
							break;
						} else {
							name_len = h - name;
							name[name_len] = '\0';
							h ++;
						}
					}
				} else {
					h = memchr(h, ';', x - h);
					if (h == NULL) {
						break;
					} else {
						h ++;
					}
				}
				while (h < x) {
					if (h[0] == ' ' || h[0] == '\t' || h[0] == '\r' || h[0] == '\n') h ++;
					else break;
				}
			}
		}
		// jump to next header unconditionally
		h = x;
	}
	*rvar = var;
	*rname = name;
	*rvar_len = var_len;
	*rname_len = name_len;
	return JS_TRUE;
}

/*
 * Read one line from data in memory
 */
char *ngx_javascript_read_line(char *s, char *e) {
	char *p = memchr(s, '\n', e - s);
	if (p != NULL) return p + 1;
	return e;
}

/*
 * Read boundary from memory
 */
JSBool ngx_javascript_read_boundary(ngx_javascript_multi_part_t *m, char *s, char **rp) {
	unsigned int hl = 0;
	char *e = m->b + m->length;
	char *p = s;
	char *n = p;
	size_t l;
	m->h = s;
	m->hl = 0;
	while (p < e) {
		n = ngx_javascript_read_line(p, e);
		l = n - p;
		if (l == 0) {
			return JS_FALSE;
		} else if (l == m->rn && hl == 0) {
			m->hl = hl = n - s;
		} else if ((size_t) (n - p) > (size_t) (m->boundary_len + 2)) {
			if (p[0] == '-' && p[1] == '-' && strncmp(p + 2, m->boundary, m->boundary_len) == 0) {
				*rp = p + 2 + m->boundary_len;
				return JS_TRUE;
			}
		}
		p = n;
	}
	return JS_FALSE;
}

/*
 * Parse multipart/form-data from a memory
 */
JSBool ngx_javascript_read_data(JSContext *jcx, JSObject *obj, ngx_javascript_client_t *client) {
	char *body;
	unsigned int body_len;
	ngx_javascript_multi_part_t mm;
	ngx_javascript_multi_part_t *m = &mm;
	char *var, *val, *name;
	size_t var_len, val_len, name_len;
	char *s, *e, *p, *n;
	memset(m, 0, sizeof(ngx_javascript_multi_part_t));
	
	if (ngx_javascript_get_boundary(client, &m->boundary, &m->boundary_len) == JS_FALSE) return JS_FALSE;
	
	body = client->body;
	body_len = client->content_length;
	
	s = body;
	e = body + body_len;
	
	m->l = m->length = e - s;
	m->b = s;
	m->client = client;
	
	// read first boundary
	if (ngx_javascript_read_boundary(m, s, &p) == JS_FALSE) goto ngx_javascript_read_multipart_form_data_finish;
	
	// determine size of end-of-line: 1 (\n) or 2 (\r\n)
	if (p[0] == '\r') {
		m->rn ++;
		p ++;
	}
	if (p[0] == '\n') {
		m->rn ++;
		p ++;
	}
	if (m->rn == 0) goto ngx_javascript_read_multipart_form_data_finish;
	
	// p points to the start of chunk - right after boundary\r\n
	while (p < e) {
		// read chunks separated by boundaries
		var = val = name = NULL;
		var_len = val_len = name_len = 0;
		// read next boundary
		if (ngx_javascript_read_boundary(m, p, &n) == JS_FALSE) goto ngx_javascript_read_multipart_form_data_finish;
		if (m->hl > 0) {
			ngx_javascript_multipart_headers(m, &var, &var_len, &name, &name_len);
			if (var != NULL) {
				val_len = n - m->h - m->hl - m->boundary_len - 2 - m->rn;
				val = m->h + m->hl;
				if (name == NULL) {
					ngx_javascript_register_variable(jcx, obj, var, var_len, val, val_len);
				} else {
					ngx_javascript_register_variable(jcx, obj, var, var_len, name, name_len);
				}
			}
		}
		p = n;
		if (e - p < 2) break;
		if (p[0] == '-' && p[1] == '-') break;
		p += m->rn;
	}
	ngx_javascript_read_multipart_form_data_finish:
	return JS_TRUE;
}

char *ngx_strlcasestrn(char *s1, char *last, char *s2, size_t n) {
	unsigned int c1, c2;
	c2 = (unsigned int) *s2++;
	c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;
	last -= n;
	do {
		do {
			if (s1 >= last) {
				return NULL;
			}
			c1 = (unsigned int) *s1++;
			c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;
		} while (c1 != c2);
	} while (ngx_strncasecmp(s1, s2, n) != 0);
	return --s1;
}

int ngx_strncasecmp(char *s1, char *s2, size_t n) {
	unsigned int c1, c2;
	while (n) {
		c1 = (unsigned int) *s1++;
		c2 = (unsigned int) *s2++;
		c1 = (c1 >= 'A' && c1 <= 'Z') ? (c1 | 0x20) : c1;
		c2 = (c2 >= 'A' && c2 <= 'Z') ? (c2 | 0x20) : c2;
		if (c1 == c2) {
			if (c1) {
				n--;
				continue;
			}
			return 0;
		}
		return c1 - c2;
	}
	return 0;
}
